feat: support upstream scheme/retries/timeouts via ingress annotations (#2614)

diff --git a/internal/adc/translator/annotations.go b/internal/adc/translator/annotations.go
index 509a093..28319b6 100644
--- a/internal/adc/translator/annotations.go
+++ b/internal/adc/translator/annotations.go
@@ -22,16 +22,24 @@
 	"github.com/imdario/mergo"
 
 	"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
+	"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/upstream"
 )
 
 // Structure extracted by Ingress Resource
-type Ingress struct{}
+type IngressConfig struct {
+	Upstream upstream.Upstream
+}
 
 // parsers registered for ingress annotations
-var ingressAnnotationParsers = map[string]annotations.IngressAnnotationsParser{}
+var ingressAnnotationParsers = map[string]annotations.IngressAnnotationsParser{
+	"upstream": upstream.NewParser(),
+}
 
-func (t *Translator) TranslateIngressAnnotations(anno map[string]string) *Ingress {
-	ing := &Ingress{}
+func (t *Translator) TranslateIngressAnnotations(anno map[string]string) *IngressConfig {
+	if len(anno) == 0 {
+		return nil
+	}
+	ing := &IngressConfig{}
 	if err := translateAnnotations(anno, ing); err != nil {
 		t.Log.Error(err, "failed to translate ingress annotations", "annotations", anno)
 	}
diff --git a/internal/adc/translator/annotations/upstream/upstream.go b/internal/adc/translator/annotations/upstream/upstream.go
new file mode 100644
index 0000000..881e02b
--- /dev/null
+++ b/internal/adc/translator/annotations/upstream/upstream.go
@@ -0,0 +1,88 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package upstream
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+	"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
+)
+
+func NewParser() annotations.IngressAnnotationsParser {
+	return &Upstream{}
+}
+
+type Upstream struct {
+	Scheme         string
+	Retries        int
+	TimeoutRead    int
+	TimeoutConnect int
+	TimeoutSend    int
+}
+
+var validSchemes = map[string]struct{}{
+	apiv2.SchemeHTTP:  {},
+	apiv2.SchemeHTTPS: {},
+	apiv2.SchemeGRPC:  {},
+	apiv2.SchemeGRPCS: {},
+}
+
+func (u Upstream) Parse(e annotations.Extractor) (any, error) {
+	if scheme := strings.ToLower(e.GetStringAnnotation(annotations.AnnotationsUpstreamScheme)); scheme != "" {
+		if _, ok := validSchemes[scheme]; ok {
+			u.Scheme = scheme
+		} else {
+			return nil, fmt.Errorf("invalid upstream scheme: %s", scheme)
+		}
+	}
+
+	if retry := e.GetStringAnnotation(annotations.AnnotationsUpstreamRetry); retry != "" {
+		t, err := strconv.Atoi(retry)
+		if err != nil {
+			return nil, fmt.Errorf("could not parse retry as an integer: %s", err.Error())
+		}
+		u.Retries = t
+	}
+
+	if timeoutConnect := strings.TrimSuffix(e.GetStringAnnotation(annotations.AnnotationsUpstreamTimeoutConnect), "s"); timeoutConnect != "" {
+		t, err := strconv.Atoi(timeoutConnect)
+		if err != nil {
+			return nil, fmt.Errorf("could not parse timeout as an integer: %s", err.Error())
+		}
+		u.TimeoutConnect = t
+	}
+
+	if timeoutRead := strings.TrimSuffix(e.GetStringAnnotation(annotations.AnnotationsUpstreamTimeoutRead), "s"); timeoutRead != "" {
+		t, err := strconv.Atoi(timeoutRead)
+		if err != nil {
+			return nil, fmt.Errorf("could not parse timeout as an integer: %s", err.Error())
+		}
+		u.TimeoutRead = t
+	}
+
+	if timeoutSend := strings.TrimSuffix(e.GetStringAnnotation(annotations.AnnotationsUpstreamTimeoutSend), "s"); timeoutSend != "" {
+		t, err := strconv.Atoi(timeoutSend)
+		if err != nil {
+			return nil, fmt.Errorf("could not parse timeout as an integer: %s", err.Error())
+		}
+		u.TimeoutSend = t
+	}
+
+	return u, nil
+}
diff --git a/internal/adc/translator/annotations/upstream/upstream_test.go b/internal/adc/translator/annotations/upstream/upstream_test.go
new file mode 100644
index 0000000..55d7459
--- /dev/null
+++ b/internal/adc/translator/annotations/upstream/upstream_test.go
@@ -0,0 +1,98 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package upstream
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
+)
+
+func TestIPRestrictionHandler(t *testing.T) {
+	anno := map[string]string{
+		annotations.AnnotationsUpstreamScheme: "grpcs",
+	}
+	u := NewParser()
+
+	out, err := u.Parse(annotations.NewExtractor(anno))
+	assert.Nil(t, err, "checking given error")
+
+	ups, ok := out.(Upstream)
+	if !ok {
+		t.Fatalf("could not parse upstream")
+	}
+	assert.Equal(t, "grpcs", ups.Scheme)
+
+	anno[annotations.AnnotationsUpstreamScheme] = "gRPC"
+	out, err = u.Parse(annotations.NewExtractor(anno))
+	ups, ok = out.(Upstream)
+	if !ok {
+		t.Fatalf("could not parse upstream")
+	}
+	assert.Nil(t, err, "checking given error")
+	assert.Equal(t, "grpc", ups.Scheme)
+
+	anno[annotations.AnnotationsUpstreamScheme] = "nothing"
+	out, err = u.Parse(annotations.NewExtractor(anno))
+	assert.NotNil(t, err, "checking given error")
+	assert.Nil(t, out, "checking given output")
+}
+
+func TestRetryParsing(t *testing.T) {
+	anno := map[string]string{
+		annotations.AnnotationsUpstreamRetry: "2",
+	}
+	u := NewParser()
+	out, err := u.Parse(annotations.NewExtractor(anno))
+	assert.Nil(t, err, "checking given error")
+	ups, ok := out.(Upstream)
+	if !ok {
+		t.Fatalf("could not parse upstream")
+	}
+	assert.Nil(t, err, "checking given error")
+	assert.Equal(t, 2, ups.Retries)
+
+	anno[annotations.AnnotationsUpstreamRetry] = "asdf"
+	out, err = u.Parse(annotations.NewExtractor(anno))
+	assert.NotNil(t, err, "checking given error")
+	assert.Nil(t, out, "checking given output")
+}
+
+func TestTimeoutParsing(t *testing.T) {
+	anno := map[string]string{
+		annotations.AnnotationsUpstreamTimeoutConnect: "2s",
+		annotations.AnnotationsUpstreamTimeoutRead:    "3s",
+		annotations.AnnotationsUpstreamTimeoutSend:    "4s",
+	}
+	u := NewParser()
+	out, err := u.Parse(annotations.NewExtractor(anno))
+	assert.Nil(t, err, "checking given error")
+
+	ups, ok := out.(Upstream)
+	if !ok {
+		t.Fatalf("could not parse upstream")
+	}
+	assert.Nil(t, err, "checking given error")
+	assert.Equal(t, 2, ups.TimeoutConnect)
+	assert.Equal(t, 3, ups.TimeoutRead)
+	assert.Equal(t, 4, ups.TimeoutSend)
+	anno[annotations.AnnotationsUpstreamRetry] = "asdf"
+	out, err = u.Parse(annotations.NewExtractor(anno))
+	assert.NotNil(t, err, "checking given error")
+	assert.Nil(t, out, "checking given output")
+}
diff --git a/internal/adc/translator/annotations_test.go b/internal/adc/translator/annotations_test.go
index d23a247..8216be3 100644
--- a/internal/adc/translator/annotations_test.go
+++ b/internal/adc/translator/annotations_test.go
@@ -22,6 +22,7 @@
 	"github.com/stretchr/testify/assert"
 
 	"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
+	"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/upstream"
 )
 
 type mockParser struct {
@@ -63,7 +64,10 @@
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			// Set up mock parsers
+			orig := ingressAnnotationParsers
+			defer func() { ingressAnnotationParsers = orig }()
+
+			ingressAnnotationParsers = make(map[string]annotations.IngressAnnotationsParser)
 			for key, parser := range tt.parsers {
 				ingressAnnotationParsers[key] = parser
 			}
@@ -77,11 +81,94 @@
 				assert.NoError(t, err)
 			}
 			assert.Equal(t, tt.expected, dst)
+		})
+	}
+}
 
-			// Clean up mock parsers
-			for key := range tt.parsers {
-				delete(ingressAnnotationParsers, key)
-			}
+func TestTranslateIngressAnnotations(t *testing.T) {
+	tests := []struct {
+		name     string
+		anno     map[string]string
+		expected *IngressConfig
+	}{
+		{
+			name:     "no matching annotations",
+			anno:     map[string]string{"upstream": "value1"},
+			expected: &IngressConfig{},
+		},
+		{
+			name:     "invalid scheme",
+			anno:     map[string]string{annotations.AnnotationsUpstreamScheme: "invalid"},
+			expected: &IngressConfig{},
+		},
+		{
+			name: "http scheme",
+			anno: map[string]string{annotations.AnnotationsUpstreamScheme: "https"},
+			expected: &IngressConfig{
+				Upstream: upstream.Upstream{
+					Scheme: "https",
+				},
+			},
+		},
+		{
+			name: "retries",
+			anno: map[string]string{annotations.AnnotationsUpstreamRetry: "3"},
+			expected: &IngressConfig{
+				Upstream: upstream.Upstream{
+					Retries: 3,
+				},
+			},
+		},
+		{
+			name: "read timeout",
+			anno: map[string]string{
+				annotations.AnnotationsUpstreamTimeoutRead: "5s",
+			},
+			expected: &IngressConfig{
+				Upstream: upstream.Upstream{
+					TimeoutRead: 5,
+				},
+			},
+		},
+		{
+			name: "timeouts",
+			anno: map[string]string{
+				annotations.AnnotationsUpstreamTimeoutRead:    "5s",
+				annotations.AnnotationsUpstreamTimeoutSend:    "6s",
+				annotations.AnnotationsUpstreamTimeoutConnect: "7s",
+			},
+			expected: &IngressConfig{
+				Upstream: upstream.Upstream{
+					TimeoutRead:    5,
+					TimeoutSend:    6,
+					TimeoutConnect: 7,
+				},
+			},
+		},
+		{
+			name: "timeout/scheme/retries",
+			anno: map[string]string{
+				annotations.AnnotationsUpstreamTimeoutRead: "5s",
+				annotations.AnnotationsUpstreamScheme:      "http",
+				annotations.AnnotationsUpstreamRetry:       "2",
+			},
+			expected: &IngressConfig{
+				Upstream: upstream.Upstream{
+					TimeoutRead: 5,
+					Scheme:      "http",
+					Retries:     2,
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			translator := &Translator{}
+			result := translator.TranslateIngressAnnotations(tt.anno)
+
+			assert.NotNil(t, result)
+			assert.Equal(t, tt.expected, result)
 		})
 	}
 }
diff --git a/internal/adc/translator/ingress.go b/internal/adc/translator/ingress.go
index 894224b..da3c5d0 100644
--- a/internal/adc/translator/ingress.go
+++ b/internal/adc/translator/ingress.go
@@ -18,6 +18,7 @@
 package translator
 
 import (
+	"cmp"
 	"fmt"
 	"strings"
 
@@ -80,6 +81,10 @@
 
 	labels := label.GenLabel(obj)
 
+	config := t.TranslateIngressAnnotations(obj.Annotations)
+
+	t.Log.V(1).Info("translating Ingress Annotations", "config", config)
+
 	// handle TLS configuration, convert to SSL objects
 	if err := t.translateIngressTLSSection(tctx, obj, result, labels); err != nil {
 		return nil, err
@@ -97,7 +102,8 @@
 		}
 
 		for j, path := range rule.HTTP.Paths {
-			if svc := t.buildServiceFromIngressPath(tctx, obj, &path, i, j, hosts, labels); svc != nil {
+			index := fmt.Sprintf("%d-%d", i, j)
+			if svc := t.buildServiceFromIngressPath(tctx, obj, config, &path, index, hosts, labels); svc != nil {
 				result.Services = append(result.Services, svc)
 			}
 		}
@@ -135,8 +141,9 @@
 func (t *Translator) buildServiceFromIngressPath(
 	tctx *provider.TranslateContext,
 	obj *networkingv1.Ingress,
+	config *IngressConfig,
 	path *networkingv1.HTTPIngressPath,
-	ruleIndex, pathIndex int,
+	index string,
 	hosts []string,
 	labels map[string]string,
 ) *adctypes.Service {
@@ -146,15 +153,15 @@
 
 	service := adctypes.NewDefaultService()
 	service.Labels = labels
-	service.Name = adctypes.ComposeServiceNameWithRule(obj.Namespace, obj.Name, fmt.Sprintf("%d-%d", ruleIndex, pathIndex))
+	service.Name = adctypes.ComposeServiceNameWithRule(obj.Namespace, obj.Name, index)
 	service.ID = id.GenID(service.Name)
 	service.Hosts = hosts
 
 	upstream := adctypes.NewDefaultUpstream()
-	protocol := t.resolveIngressUpstream(tctx, obj, path.Backend.Service, upstream)
+	protocol := t.resolveIngressUpstream(tctx, obj, config, path.Backend.Service, upstream)
 	service.Upstream = upstream
 
-	route := buildRouteFromIngressPath(obj, path, ruleIndex, pathIndex, labels)
+	route := buildRouteFromIngressPath(obj, path, index, labels)
 	if protocol == internaltypes.AppProtocolWS || protocol == internaltypes.AppProtocolWSS {
 		route.EnableWebsocket = ptr.To(true)
 	}
@@ -167,11 +174,28 @@
 func (t *Translator) resolveIngressUpstream(
 	tctx *provider.TranslateContext,
 	obj *networkingv1.Ingress,
+	config *IngressConfig,
 	backendService *networkingv1.IngressServiceBackend,
 	upstream *adctypes.Upstream,
 ) string {
 	backendRef := convertBackendRef(obj.Namespace, backendService.Name, internaltypes.KindService)
 	t.AttachBackendTrafficPolicyToUpstream(backendRef, tctx.BackendTrafficPolicies, upstream)
+	if config != nil {
+		upConfig := config.Upstream
+		if upConfig.Scheme != "" {
+			upstream.Scheme = upConfig.Scheme
+		}
+		if upConfig.Retries > 0 {
+			upstream.Retries = ptr.To(int64(upConfig.Retries))
+		}
+		if upConfig.TimeoutConnect > 0 || upConfig.TimeoutRead > 0 || upConfig.TimeoutSend > 0 {
+			upstream.Timeout = &adctypes.Timeout{
+				Connect: cmp.Or(upConfig.TimeoutConnect, 60),
+				Read:    cmp.Or(upConfig.TimeoutRead, 60),
+				Send:    cmp.Or(upConfig.TimeoutSend, 60),
+			}
+		}
+	}
 	// determine service port/port name
 	var protocol string
 	var port intstr.IntOrString
@@ -224,11 +248,11 @@
 func buildRouteFromIngressPath(
 	obj *networkingv1.Ingress,
 	path *networkingv1.HTTPIngressPath,
-	ruleIndex, pathIndex int,
+	index string,
 	labels map[string]string,
 ) *adctypes.Route {
 	route := adctypes.NewDefaultRoute()
-	route.Name = adctypes.ComposeRouteName(obj.Namespace, obj.Name, fmt.Sprintf("%d-%d", ruleIndex, pathIndex))
+	route.Name = adctypes.ComposeRouteName(obj.Namespace, obj.Name, index)
 	route.ID = id.GenID(route.Name)
 	route.Labels = labels
 
diff --git a/internal/webhook/v1/ingress_webhook.go b/internal/webhook/v1/ingress_webhook.go
index 03ee8f5..4941a82 100644
--- a/internal/webhook/v1/ingress_webhook.go
+++ b/internal/webhook/v1/ingress_webhook.go
@@ -42,11 +42,6 @@
 	"k8s.apisix.apache.org/use-regex",
 	"k8s.apisix.apache.org/enable-websocket",
 	"k8s.apisix.apache.org/plugin-config-name",
-	"k8s.apisix.apache.org/upstream-scheme",
-	"k8s.apisix.apache.org/upstream-retries",
-	"k8s.apisix.apache.org/upstream-connect-timeout",
-	"k8s.apisix.apache.org/upstream-read-timeout",
-	"k8s.apisix.apache.org/upstream-send-timeout",
 	"k8s.apisix.apache.org/enable-cors",
 	"k8s.apisix.apache.org/cors-allow-origin",
 	"k8s.apisix.apache.org/cors-allow-headers",
diff --git a/test/e2e/framework/manifests/nginx.yaml b/test/e2e/framework/manifests/nginx.yaml
index a01d554..e75c1da 100644
--- a/test/e2e/framework/manifests/nginx.yaml
+++ b/test/e2e/framework/manifests/nginx.yaml
@@ -43,6 +43,14 @@
           return 200 'Hello, World!';
         }
 
+        location /delay {
+          content_by_lua_block {
+            local delay = tonumber(ngx.var.arg_delay) or 0
+            ngx.sleep(delay)
+            ngx.say("Slept for ", delay, " seconds")
+          }
+        }
+
         location /ws {
           content_by_lua_block {
             local server = require "resty.websocket.server"
@@ -144,6 +152,10 @@
       protocol: TCP
       targetPort: 443
       appProtocol: https
+    - name: https-v2
+      port: 7443
+      protocol: TCP
+      targetPort: 443
     - name: ws
       port: 8080
       protocol: TCP
diff --git a/test/e2e/ingress/annotations.go b/test/e2e/ingress/annotations.go
new file mode 100644
index 0000000..3043d5b
--- /dev/null
+++ b/test/e2e/ingress/annotations.go
@@ -0,0 +1,171 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package ingress
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"k8s.io/utils/ptr"
+
+	"github.com/apache/apisix-ingress-controller/test/e2e/framework"
+	"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+)
+
+var _ = Describe("Test Ingress With Annotations", Label("networking.k8s.io", "ingress"), func() {
+	s := scaffold.NewDefaultScaffold()
+
+	Context("Upstream", func() {
+		var (
+			ingressRetries = `
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: retries
+  annotations:
+    k8s.apisix.apache.org/upstream-retries: "3"
+spec:
+  ingressClassName: %s
+  rules:
+  - host: nginx.example
+    http:
+      paths:
+      - path: /get
+        pathType: Exact
+        backend:
+          service:
+            name: nginx
+            port:
+              number: 80
+`
+			ingressSchemeHTTPS = `
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: https-backend
+  annotations:
+    k8s.apisix.apache.org/upstream-scheme: https
+spec:
+  ingressClassName: %s
+  rules:
+  - host: nginx.example
+    http:
+      paths:
+      - path: /get
+        pathType: Exact
+        backend:
+          service:
+            name: nginx
+            port:
+              number: 7443
+`
+
+			ingressTimeouts = `
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: timeouts
+  annotations:
+    k8s.apisix.apache.org/upstream-read-timeout: "2s"
+    k8s.apisix.apache.org/upstream-send-timeout: "3s"
+    k8s.apisix.apache.org/upstream-connect-timeout: "4s"
+spec:
+  ingressClassName: %s
+  rules:
+  - host: nginx.example
+    http:
+      paths:
+      - path: /delay
+        pathType: Exact
+        backend:
+          service:
+            name: nginx
+            port:
+              number: 443
+`
+		)
+		BeforeEach(func() {
+			s.DeployNginx(framework.NginxOptions{
+				Namespace: s.Namespace(),
+				Replicas:  ptr.To(int32(1)),
+			})
+			By("create GatewayProxy")
+			Expect(s.CreateResourceFromString(s.GetGatewayProxySpec())).NotTo(HaveOccurred(), "creating GatewayProxy")
+
+			By("create IngressClass")
+			err := s.CreateResourceFromStringWithNamespace(s.GetIngressClassYaml(), "")
+			Expect(err).NotTo(HaveOccurred(), "creating IngressClass")
+			time.Sleep(5 * time.Second)
+		})
+		It("retries", func() {
+			Expect(s.CreateResourceFromString(fmt.Sprintf(ingressRetries, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
+
+			s.RequestAssert(&scaffold.RequestAssert{
+				Method: "GET",
+				Path:   "/get",
+				Host:   "nginx.example",
+				Check:  scaffold.WithExpectedStatus(http.StatusOK),
+			})
+			upstreams, err := s.DefaultDataplaneResource().Upstream().List(context.Background())
+			Expect(err).NotTo(HaveOccurred(), "listing Upstream")
+			Expect(upstreams).To(HaveLen(1), "checking Upstream length")
+			Expect(upstreams[0].Retries).To(Equal(ptr.To(int64(3))), "checking Upstream retries")
+		})
+		It("scheme", func() {
+			Expect(s.CreateResourceFromString(fmt.Sprintf(ingressSchemeHTTPS, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
+
+			s.RequestAssert(&scaffold.RequestAssert{
+				Method: "GET",
+				Path:   "/get",
+				Host:   "nginx.example",
+				Check:  scaffold.WithExpectedStatus(http.StatusOK),
+			})
+			upstreams, err := s.DefaultDataplaneResource().Upstream().List(context.Background())
+			Expect(err).NotTo(HaveOccurred(), "listing Upstream")
+			Expect(upstreams).To(HaveLen(1), "checking Upstream length")
+			Expect(upstreams[0].Scheme).To(Equal("https"), "checking Upstream scheme")
+		})
+		It("timeouts", func() {
+			Expect(s.CreateResourceFromString(fmt.Sprintf(ingressTimeouts, s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
+
+			s.RequestAssert(&scaffold.RequestAssert{
+				Method: "GET",
+				Path:   "/delay",
+				Host:   "nginx.example",
+				Check:  scaffold.WithExpectedStatus(http.StatusOK),
+			})
+
+			_ = s.NewAPISIXClient().GET("/delay").WithQuery("delay", "10").
+				WithHost("nginx.example").Expect().Status(http.StatusGatewayTimeout)
+
+			_ = s.NewAPISIXClient().GET("/delay").WithHost("nginx.example").Expect().Status(http.StatusOK)
+
+			upstreams, err := s.DefaultDataplaneResource().Upstream().List(context.Background())
+			Expect(err).NotTo(HaveOccurred(), "listing Upstream")
+			Expect(upstreams).To(HaveLen(1), "checking Upstream length")
+			Expect(upstreams[0].Timeout).ToNot(BeNil(), "checking Upstream timeout")
+			Expect(upstreams[0].Timeout.Read).To(Equal(2), "checking Upstream read timeout")
+			Expect(upstreams[0].Timeout.Send).To(Equal(3), "checking Upstream send timeout")
+			Expect(upstreams[0].Timeout.Connect).To(Equal(4), "checking Upstream connect timeout")
+		})
+	})
+})