Repository: elastic/elasticsearch-py

Issue:
Calling options() on a client resets timeout related attributes
<!-- Bug report -->

**Elasticsearch version** (8.1.1):

**`elasticsearch-py` version (`8.1.1`)**: 

Please make sure the major version matches the Elasticsearch server you are running.

**Description of the problem including expected versus actual behavior**:

When the `options()` method is called for an existing `ElasticSearch` client instance, the timeout related parameters are not preserved. These parameters are:

- `self._request_timeout`
- `self._max_retries`
- `self._retry_on_timeout`
- `self._retry_on_status`

This is problematic in the `helpers` module, particularly `scan()` because that method calls the `options` and resets any timeout related settings we may have made on the client. We can re-specify the `request_timeout` there but retries parameters cannot be replicated

**Steps to reproduce**:
```
>>> import elasticsearch as ES
>>> es=ES.Elasticsearch("http://esmasters:9200", sniff_on_connection_fail=True, sniff_on_start=True, min_delay_between_sniffing=600, request_timeout=600, sniff_timeout=300, max_retries=5, retry_on_timeout=True)
>>> es._retry_on_timeout
True
>>> es2=es.options()
>>> es2._retry_on_timeout
<DEFAULT>
>>> es._max_retries
5
>>> es2._max_retries
<DEFAULT>
>>> es2._retry_on_status
<DEFAULT>
>>> es._retry_on_status
<DEFAULT>
>>> es._request_timeout
600
>>> es2._request_timeout
<DEFAULT>
```


Comments:

Patch:
diff --git a/elasticsearch/_async/client/__init__.py b/elasticsearch/_async/client/__init__.py
index aae2173e..9ec82bf2 100644
--- a/elasticsearch/_async/client/__init__.py
+++ b/elasticsearch/_async/client/__init__.py
@@ -519,26 +519,36 @@ class AsyncElasticsearch(BaseClient):
 
         if request_timeout is not DEFAULT:
             client._request_timeout = request_timeout
+        else:
+            client._request_timeout = self._request_timeout
 
         if ignore_status is not DEFAULT:
             if isinstance(ignore_status, int):
                 ignore_status = (ignore_status,)
             client._ignore_status = ignore_status
+        else:
+            client._ignore_status = self._ignore_status
 
         if max_retries is not DEFAULT:
             if not isinstance(max_retries, int):
                 raise TypeError("'max_retries' must be of type 'int'")
             client._max_retries = max_retries
+        else:
+            client._max_retries = self._max_retries
 
         if retry_on_status is not DEFAULT:
             if isinstance(retry_on_status, int):
                 retry_on_status = (retry_on_status,)
             client._retry_on_status = retry_on_status
+        else:
+            client._retry_on_status = self._retry_on_status
 
         if retry_on_timeout is not DEFAULT:
             if not isinstance(retry_on_timeout, bool):
                 raise TypeError("'retry_on_timeout' must be of type 'bool'")
             client._retry_on_timeout = retry_on_timeout
+        else:
+            client._retry_on_timeout = self._retry_on_timeout
 
         return client
 
diff --git a/elasticsearch/_sync/client/__init__.py b/elasticsearch/_sync/client/__init__.py
index c1860766..af10f2db 100644
--- a/elasticsearch/_sync/client/__init__.py
+++ b/elasticsearch/_sync/client/__init__.py
@@ -519,26 +519,36 @@ class Elasticsearch(BaseClient):
 
         if request_timeout is not DEFAULT:
             client._request_timeout = request_timeout
+        else:
+            client._request_timeout = self._request_timeout
 
         if ignore_status is not DEFAULT:
             if isinstance(ignore_status, int):
                 ignore_status = (ignore_status,)
             client._ignore_status = ignore_status
+        else:
+            client._ignore_status = self._ignore_status
 
         if max_retries is not DEFAULT:
             if not isinstance(max_retries, int):
                 raise TypeError("'max_retries' must be of type 'int'")
             client._max_retries = max_retries
+        else:
+            client._max_retries = self._max_retries
 
         if retry_on_status is not DEFAULT:
             if isinstance(retry_on_status, int):
                 retry_on_status = (retry_on_status,)
             client._retry_on_status = retry_on_status
+        else:
+            client._retry_on_status = self._retry_on_status
 
         if retry_on_timeout is not DEFAULT:
             if not isinstance(retry_on_timeout, bool):
                 raise TypeError("'retry_on_timeout' must be of type 'bool'")
             client._retry_on_timeout = retry_on_timeout
+        else:
+            client._retry_on_timeout = self._retry_on_timeout
 
         return client
 


Test patch:
diff --git a/test_elasticsearch/test_client/test_options.py b/test_elasticsearch/test_client/test_options.py
index 16e89af5..72d5edf4 100644
--- a/test_elasticsearch/test_client/test_options.py
+++ b/test_elasticsearch/test_client/test_options.py
@@ -368,3 +368,104 @@ class TestOptions(DummyTransportTestCase):
             "user-agent": "custom4",
             "accept": "application/vnd.elasticsearch+json; compatible-with=8",
         }
+
+    def test_options_timeout_parameters(self):
+        client = Elasticsearch(
+            "http://localhost:9200",
+            transport_class=DummyTransport,
+            request_timeout=1,
+            max_retries=2,
+            retry_on_status=(404,),
+            retry_on_timeout=True,
+        )
+
+        # timeout parameters are preserved with .options()
+        client.options().indices.get(index="test")
+
+        calls = client.transport.calls
+        call = calls[("GET", "/test")][0]
+        assert call.pop("client_meta") is DEFAULT
+        assert call == {
+            "headers": {
+                "accept": "application/vnd.elasticsearch+json; compatible-with=8",
+            },
+            "body": None,
+            "request_timeout": 1,
+            "max_retries": 2,
+            "retry_on_status": (404,),
+            "retry_on_timeout": True,
+        }
+
+        client = Elasticsearch(
+            "http://localhost:9200",
+            transport_class=DummyTransport,
+            request_timeout=1,
+            max_retries=2,
+            retry_on_status=(404,),
+            retry_on_timeout=True,
+        )
+        client.options(
+            request_timeout=2,
+            max_retries=3,
+            retry_on_status=(400,),
+            retry_on_timeout=False,
+        ).indices.get(index="test")
+
+        calls = client.transport.calls
+        call = calls[("GET", "/test")][0]
+        assert call.pop("client_meta") is DEFAULT
+        assert call == {
+            "headers": {
+                "accept": "application/vnd.elasticsearch+json; compatible-with=8",
+            },
+            "body": None,
+            "request_timeout": 2,
+            "max_retries": 3,
+            "retry_on_status": (400,),
+            "retry_on_timeout": False,
+        }
+
+        client = Elasticsearch(
+            "http://localhost:9200",
+            transport_class=DummyTransport,
+        )
+        client.options().indices.get(index="test")
+
+        calls = client.transport.calls
+        call = calls[("GET", "/test")][0]
+        assert call.pop("request_timeout") is DEFAULT
+        assert call.pop("max_retries") is DEFAULT
+        assert call.pop("retry_on_timeout") is DEFAULT
+        assert call.pop("retry_on_status") is DEFAULT
+        assert call.pop("client_meta") is DEFAULT
+        assert call == {
+            "headers": {
+                "accept": "application/vnd.elasticsearch+json; compatible-with=8",
+            },
+            "body": None,
+        }
+
+        client = Elasticsearch(
+            "http://localhost:9200",
+            transport_class=DummyTransport,
+        )
+        client.options(
+            request_timeout=1,
+            max_retries=2,
+            retry_on_status=(404,),
+            retry_on_timeout=True,
+        ).indices.get(index="test")
+
+        calls = client.transport.calls
+        call = calls[("GET", "/test")][0]
+        assert call.pop("client_meta") is DEFAULT
+        assert call == {
+            "headers": {
+                "accept": "application/vnd.elasticsearch+json; compatible-with=8",
+            },
+            "body": None,
+            "request_timeout": 1,
+            "max_retries": 2,
+            "retry_on_status": (404,),
+            "retry_on_timeout": True,
+        }