Repository: ethereum/web3.py

Issue:
BeaconAPI timed out within 10 seconds, unable to set timeout time
*OSX
* Python: 3.11.2

```
pip freeze
aiohttp==3.8.4
aiosignal==1.3.1
async-timeout==4.0.2
attrs==22.2.0
bitarray==2.7.3
certifi==2022.12.7
charset-normalizer==3.1.0
cytoolz==0.12.1
docutils==0.19
eth-abi==4.0.0
eth-account==0.8.0
eth-hash==0.5.1
eth-keyfile==0.6.1
eth-keys==0.4.0
eth-rlp==0.3.0
eth-typing==3.3.0
eth-utils==2.1.0
frozenlist==1.3.3
hexbytes==0.3.0
idna==3.4
jsonschema==4.17.3
lru-dict==1.1.8
multidict==6.0.4
parsimonious==0.9.0
protobuf==4.22.3
pycryptodome==3.17
pyrsistent==0.19.3
ranger-fm==1.9.3
regex==2023.3.23
requests==2.28.2
rlp==3.0.0
six==1.16.0
toolz==0.12.0
urllib3==1.26.15
web3==6.2.0
websockets==11.0.1
yarl==1.8.2
```


### What was wrong?
BeaconAPI timed out within 10 seconds, unable to set timeout time
```
clHttpProvider = HTTPProvider(clEndpoint,request_kwargs={'timeout': 60})
elHttpProvider = HTTPProvider(elEndpoint,request_kwargs={'timeout': 60})

w3 = Web3(elHttpProvider)
print(w3.is_connected())


beacon = Beacon(clEndpoint)
# Beacon()


validators = beacon.get_validators()
print(validators)

```

error: timeout



### How can it be fixed?
The timeout should be able to be set like an httpProvider




Discussion:

Patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 81376ca5..bc2b9924 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 6.9.0
+current_version = 6.10.0
 commit = True
 tag = True
 parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-(?P<stage>[^.]*)\.(?P<devnum>\d+))?
diff --git a/docs/contributing.rst b/docs/contributing.rst
index 4b9b73e5..b6df22b6 100644
--- a/docs/contributing.rst
+++ b/docs/contributing.rst
@@ -67,7 +67,7 @@ To start up the test environment, run:
 
 .. code:: sh
 
-    $ docker-compose up -d
+    $ docker compose up -d
 
 
 This will build a Docker container set up with an environment to run the
@@ -82,7 +82,7 @@ To run the Python tests from your local machine:
 
 .. code:: sh
 
-    $ docker-compose exec sandbox bash -c 'pytest -n 4 -f -k "not goethereum"'
+    $ docker compose exec sandbox bash -c 'pytest -n 4 -f -k "not goethereum"'
 
 
 You can run arbitrary commands inside the Docker container by using the
@@ -90,14 +90,14 @@ You can run arbitrary commands inside the Docker container by using the
 
 .. code:: sh
 
-    $ docker-compose exec sandbox bash -c ''
+    $ docker compose exec sandbox bash -c ''
 
 
 Or, if you would like to open a session to the container, run:
 
 .. code:: sh
 
-    $ docker-compose exec sandbox bash
+    $ docker compose exec sandbox bash
 
 
 Code Style
diff --git a/docs/releases.rst b/docs/releases.rst
index 1838f6da..d361b3cf 100644
--- a/docs/releases.rst
+++ b/docs/releases.rst
@@ -4,12 +4,48 @@ Release Notes
 v6 Breaking Changes Summary
    See the :ref:`v6 Migration Guide<migrating_v5_to_v6>`
 
-.. note::
-   web3.py v5 support will end on August 31, 2023.
-   See issue `#3044 <https://github.com/ethereum/web3.py/issues/3044>`__
-
 .. towncrier release notes start
 
+web3.py v6.10.0 (2023-09-21)
+----------------------------
+
+Breaking Changes (to Beta APIs)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- Breaking change to the API for interacting with a persistent websocket connection via ``AsyncWeb3`` and ``WebsocketProviderV2``. This change internalizes the ``provider.ws`` property and opts for a ``w3.ws`` API achieved via a new ``WebsocketConnection`` class. With these changes, ``eth_subscription`` messages now return the subscription id as the ``subscription`` param and the formatted message as the ``result`` param. (`#3096 <https://github.com/ethereum/web3.py/issues/3096>`__)
+
+
+Bugfixes
+~~~~~~~~
+
+- Return `w3.eth.gas_price` when calculating time based gas price strategy for an empty chain. (`#1149 <https://github.com/ethereum/web3.py/issues/1149>`__)
+- Update `LogReceipt` and `TxReceipt` declarations. Remove `LogReceipt`'s `payload` and `topic` attributes. Refactor `LogEntry` to `LogReceipt`. (`#3043 <https://github.com/ethereum/web3.py/issues/3043>`__)
+- Fixes ``AsyncEth.max_priority_fee_per_gas``. It wasn't falling back to ``eth_feeHistory`` since the ``MethodUnavailable`` error was introduced. (`#3084 <https://github.com/ethereum/web3.py/issues/3084>`__)
+
+
+Improved Documentation
+~~~~~~~~~~~~~~~~~~~~~~
+
+- Update ``WebsocketProviderV2`` documentation to reflect the new public websocket API via the ``WebsocketConnection`` class. (`#3096 <https://github.com/ethereum/web3.py/issues/3096>`__)
+
+
+Features
+~~~~~~~~
+
+- Improved error messaging for exceptions from malformed JSON-RPC responses. (`#3053 <https://github.com/ethereum/web3.py/issues/3053>`__)
+- Enable filtering by non-indexed arguments for contract event ``get_logs()``. (`#3078 <https://github.com/ethereum/web3.py/issues/3078>`__)
+- Add ``eth_maxPriorityFeePerGas`` to ``exception_retry_middleware`` whitelist (`#3090 <https://github.com/ethereum/web3.py/issues/3090>`__)
+- Sync responses for ``WebsocketProviderV2`` open connections with requests via matching RPC ``id`` values. (`#3096 <https://github.com/ethereum/web3.py/issues/3096>`__)
+- Properly JSON encode ``AttributeDict``, ``bytes``, and ``HexBytes`` when sending a JSON-RPC request by utilizing the in-house ``Web3JsonEncoder`` class. (`#3101 <https://github.com/ethereum/web3.py/issues/3101>`__)
+
+
+Internal Changes - for web3.py Contributors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- Fix an issue with an IPC test present only on MacOSX. (`#929 <https://github.com/ethereum/web3.py/issues/929>`__)
+- Ignore flake8 rule F401 (unused import) in all ``__init__.py`` files (`#3097 <https://github.com/ethereum/web3.py/issues/3097>`__)
+
+
 web3.py v6.9.0 (2023-08-23)
 ---------------------------
 
diff --git a/newsfragments/1149.bugfix.rst b/newsfragments/1149.bugfix.rst
deleted file mode 100644
index d9071f5d..00000000
--- a/newsfragments/1149.bugfix.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Return `w3.eth.gas_price` when calculating time based gas price strategy for an empty chain.
\ No newline at end of file
diff --git a/newsfragments/3043.bugfix.rst b/newsfragments/3043.bugfix.rst
deleted file mode 100644
index f02868c4..00000000
--- a/newsfragments/3043.bugfix.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Update `LogReceipt` and `TxReceipt` declarations. Remove `LogReceipt`'s `payload` and `topic` attributes. Refactor `LogEntry` to `LogReceipt`.
diff --git a/newsfragments/3053.feature.rst b/newsfragments/3053.feature.rst
deleted file mode 100644
index 8d23689a..00000000
--- a/newsfragments/3053.feature.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Improved error messaging for exceptions from malformed JSON-RPC responses.
\ No newline at end of file
diff --git a/newsfragments/3078.feature.rst b/newsfragments/3078.feature.rst
deleted file mode 100644
index 87149658..00000000
--- a/newsfragments/3078.feature.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Enable filtering by non-indexed arguments for contract event ``get_logs()``.
diff --git a/newsfragments/3084.bugfix.rst b/newsfragments/3084.bugfix.rst
deleted file mode 100644
index f87ce654..00000000
--- a/newsfragments/3084.bugfix.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Fixes ``AsyncEth.max_priority_fee_per_gas``. It wasn't falling back to ``eth_feeHistory`` since the ``MethodUnavailable`` error was introduced.
\ No newline at end of file
diff --git a/newsfragments/3090.feature.rst b/newsfragments/3090.feature.rst
deleted file mode 100644
index 6c7c2688..00000000
--- a/newsfragments/3090.feature.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Add ``eth_maxPriorityFeePerGas`` to ``exception_retry_middleware`` whitelist
diff --git a/newsfragments/3096.breaking.rst b/newsfragments/3096.breaking.rst
deleted file mode 100644
index 4731e4e5..00000000
--- a/newsfragments/3096.breaking.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Breaking change to the API for interacting with a persistent websocket connection via ``AsyncWeb3`` and ``WebsocketProviderV2``. This change internalizes the ``provider.ws`` property and opts for a ``w3.ws`` API achieved via a new ``WebsocketConnection`` class. With these changes, ``eth_subscription`` messages now return the subscription id as the ``subscription`` param and the formatted message as the ``result`` param.
diff --git a/newsfragments/3096.docs.rst b/newsfragments/3096.docs.rst
deleted file mode 100644
index 14a50066..00000000
--- a/newsfragments/3096.docs.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Update ``WebsocketProviderV2`` documentation to reflect the new public websocket API via the ``WebsocketConnection`` class.
diff --git a/newsfragments/3096.feature.rst b/newsfragments/3096.feature.rst
deleted file mode 100644
index f8624679..00000000
--- a/newsfragments/3096.feature.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Sync responses for ``WebsocketProviderV2`` open connections with requests via matching RPC ``id`` values.
diff --git a/newsfragments/3097.internal.rst b/newsfragments/3097.internal.rst
deleted file mode 100644
index 10f9db93..00000000
--- a/newsfragments/3097.internal.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Ignore flake8 rule F401 (unused import) in all ``__init__.py`` files
diff --git a/newsfragments/3101.feature.rst b/newsfragments/3101.feature.rst
deleted file mode 100644
index 141022b9..00000000
--- a/newsfragments/3101.feature.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Properly JSON encode ``AttributeDict``, ``bytes``, and ``HexBytes`` when sending a JSON-RPC request by utilizing the in-house ``Web3JsonEncoder`` class.
diff --git a/newsfragments/3104.bugfix.rst b/newsfragments/3104.bugfix.rst
new file mode 100644
index 00000000..deaf0a8a
--- /dev/null
+++ b/newsfragments/3104.bugfix.rst
@@ -0,0 +1,1 @@
+Use hashes to compare equality of two ``AttributeDict``s
diff --git a/newsfragments/3106.feature.rst b/newsfragments/3106.feature.rst
new file mode 100644
index 00000000..4f01347a
--- /dev/null
+++ b/newsfragments/3106.feature.rst
@@ -0,0 +1,1 @@
+Allow passing in a ``float`` for a ``request_timeout`` for requests for the ``Beacon`` class. Update some Beacon API endpoints (sync and async).
diff --git a/newsfragments/3107.docs.rst b/newsfragments/3107.docs.rst
new file mode 100644
index 00000000..7a23e7e5
--- /dev/null
+++ b/newsfragments/3107.docs.rst
@@ -0,0 +1,1 @@
+Change ``docker-compose`` to ``docker compose`` in the Contributing docs examples.
\ No newline at end of file
diff --git a/newsfragments/929.internal.rst b/newsfragments/929.internal.rst
deleted file mode 100644
index 0e6f855d..00000000
--- a/newsfragments/929.internal.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Fix an issue with an IPC test present only on MacOSX.
diff --git a/setup.py b/setup.py
index 6584cb02..347f0689 100644
--- a/setup.py
+++ b/setup.py
@@ -59,7 +59,7 @@ with open("./README.md") as readme:
 setup(
     name="web3",
     # *IMPORTANT*: Don't manually change the version here. Use the 'bumpversion' utility.
-    version="6.9.0",
+    version="6.10.0",
     description="""web3.py""",
     long_description_content_type="text/markdown",
     long_description=long_description,
diff --git a/web3/_utils/async_transactions.py b/web3/_utils/async_transactions.py
index f602d8e4..d65e160c 100644
--- a/web3/_utils/async_transactions.py
+++ b/web3/_utils/async_transactions.py
@@ -1,16 +1,10 @@
 from typing import (
     TYPE_CHECKING,
-    Any,
-    Dict,
     Optional,
     cast,
 )
 
-from eth_abi import (
-    abi,
-)
 from eth_typing import (
-    URI,
     ChecksumAddress,
 )
 from eth_utils.toolz import (
@@ -21,27 +15,15 @@ from hexbytes import (
     HexBytes,
 )
 
-from web3._utils.request import (
-    async_get_json_from_client_response,
-    async_get_response_from_get_request,
-    async_get_response_from_post_request,
-)
 from web3._utils.transactions import (
     prepare_replacement_transaction,
 )
-from web3._utils.type_conversion import (
-    to_bytes_if_hex,
-    to_hex_if_bytes,
-)
 from web3._utils.utility_methods import (
     any_in_dict,
 )
 from web3.constants import (
     DYNAMIC_FEE_TXN_PARAMS,
 )
-from web3.exceptions import (
-    Web3ValidationError,
-)
 from web3.types import (
     BlockIdentifier,
     TxData,
@@ -168,73 +150,6 @@ async def async_fill_transaction_defaults(
     return merge(defaults, transaction)
 
 
-async def async_handle_offchain_lookup(
-    offchain_lookup_payload: Dict[str, Any],
-    transaction: TxParams,
-) -> bytes:
-    formatted_sender = to_hex_if_bytes(offchain_lookup_payload["sender"]).lower()
-    formatted_data = to_hex_if_bytes(offchain_lookup_payload["callData"]).lower()
-
-    if formatted_sender != to_hex_if_bytes(transaction["to"]).lower():
-        raise Web3ValidationError(
-            "Cannot handle OffchainLookup raised inside nested call. Returned `sender` "
-            "value does not equal `to` address in transaction."
-        )
-
-    for url in offchain_lookup_payload["urls"]:
-        formatted_url = URI(
-            str(url)
-            .replace("{sender}", str(formatted_sender))
-            .replace("{data}", str(formatted_data))
-        )
-
-        try:
-            if "{data}" in url and "{sender}" in url:
-                response = await async_get_response_from_get_request(formatted_url)
-            elif "{sender}" in url:
-                response = await async_get_response_from_post_request(
-                    formatted_url,
-                    data={"data": formatted_data, "sender": formatted_sender},
-                )
-            else:
-                raise Web3ValidationError("url not formatted properly.")
-        except Exception:
-            continue  # try next url if timeout or issues making the request
-
-        if (
-            400 <= response.status <= 499
-        ):  # if request returns 400 error, raise exception
-            response.raise_for_status()
-        if not 200 <= response.status <= 299:  # if not 400 error, try next url
-            continue
-
-        result = await async_get_json_from_client_response(response)
-
-        if "data" not in result.keys():
-            raise Web3ValidationError(
-                "Improperly formatted response for offchain lookup HTTP request "
-                "- missing 'data' field."
-            )
-
-        encoded_data_with_function_selector = b"".join(
-            [
-                # 4-byte callback function selector
-                to_bytes_if_hex(offchain_lookup_payload["callbackFunction"]),
-                # encode the `data` from the result and the `extraData` as bytes
-                abi.encode(
-                    ["bytes", "bytes"],
-                    [
-                        to_bytes_if_hex(result["data"]),
-                        to_bytes_if_hex(offchain_lookup_payload["extraData"]),
-                    ],
-                ),
-            ]
-        )
-
-        return encoded_data_with_function_selector
-    raise Exception("Offchain lookup failed for supplied urls.")
-
-
 async def async_get_required_transaction(
     async_w3: "AsyncWeb3", transaction_hash: _Hash32
 ) -> TxData:
diff --git a/web3/beacon/api_endpoints.py b/web3/beacon/api_endpoints.py
index e94c9ef9..570f8218 100644
--- a/web3/beacon/api_endpoints.py
+++ b/web3/beacon/api_endpoints.py
@@ -1,6 +1,8 @@
 # [ BEACON endpoints ]
 
 GET_GENESIS = "/eth/v1/beacon/genesis"
+
+# states
 GET_HASH_ROOT = "/eth/v1/beacon/states/{0}/root"
 GET_FORK_DATA = "/eth/v1/beacon/states/{0}/fork"
 GET_FINALITY_CHECKPOINT = "/eth/v1/beacon/states/{0}/finality_checkpoints"
@@ -8,15 +10,35 @@ GET_VALIDATORS = "/eth/v1/beacon/states/{0}/validators"
 GET_VALIDATOR = "/eth/v1/beacon/states/{0}/validators/{1}"
 GET_VALIDATOR_BALANCES = "/eth/v1/beacon/states/{0}/validator_balances"
 GET_EPOCH_COMMITTEES = "/eth/v1/beacon/states/{0}/committees"
+GET_EPOCH_SYNC_COMMITTEES = "/eth/v1/beacon/states/{0}/sync_committees"
+GET_EPOCH_RANDAO = "/eth/v1/beacon/states/{0}/randao"
+
+# headers
 GET_BLOCK_HEADERS = "/eth/v1/beacon/headers"
 GET_BLOCK_HEADER = "/eth/v1/beacon/headers/{0}"
+
+# blocks
 GET_BLOCK = "/eth/v2/beacon/blocks/{0}"
 GET_BLOCK_ROOT = "/eth/v1/beacon/blocks/{0}/root"
 GET_BLOCK_ATTESTATIONS = "/eth/v1/beacon/blocks/{0}/attestations"
+GET_BLINDED_BLOCKS = "/eth/v1/beacon/blinded_blocks/{0}"
+
+# rewards
+GET_REWARDS = "/eth/v1/beacon/rewards/blocks/{0}"
+
+# light client
+GET_LIGHT_CLIENT_BOOTSTRAP_STRUCTURE = "/eth/v1/beacon/light_client/bootstrap/{0}"
+GET_LIGHT_CLIENT_UPDATES = "/eth/v1/beacon/light_client/updates"
+GET_LIGHT_CLIENT_FINALITY_UPDATE = "/eth/v1/beacon/light_client/finality_update"
+GET_LIGHT_CLIENT_OPTIMISTIC_UPDATE = "/eth/v1/beacon/light_client/optimistic_update"
+
+# pool
 GET_ATTESTATIONS = "/eth/v1/beacon/pool/attestations"
 GET_ATTESTER_SLASHINGS = "/eth/v1/beacon/pool/attester_slashings"
 GET_PROPOSER_SLASHINGS = "/eth/v1/beacon/pool/proposer_slashings"
 GET_VOLUNTARY_EXITS = "/eth/v1/beacon/pool/voluntary_exits"
+GET_BLS_TO_EXECUTION_CHANGES = "/eth/v1/beacon/pool/bls_to_execution_changes"
+
 
 # [ CONFIG endpoints ]
 
diff --git a/web3/beacon/async_beacon.py b/web3/beacon/async_beacon.py
index bff683a9..c2d2e202 100644
--- a/web3/beacon/async_beacon.py
+++ b/web3/beacon/async_beacon.py
@@ -5,6 +5,7 @@ from typing import (
 
 from eth_typing import (
     URI,
+    HexStr,
 )
 
 from web3._utils.request import (
@@ -16,23 +17,32 @@ from web3.beacon.api_endpoints import (
     GET_ATTESTER_SLASHINGS,
     GET_BEACON_HEADS,
     GET_BEACON_STATE,
+    GET_BLINDED_BLOCKS,
     GET_BLOCK,
     GET_BLOCK_ATTESTATIONS,
     GET_BLOCK_HEADER,
     GET_BLOCK_HEADERS,
     GET_BLOCK_ROOT,
+    GET_BLS_TO_EXECUTION_CHANGES,
     GET_DEPOSIT_CONTRACT,
     GET_EPOCH_COMMITTEES,
+    GET_EPOCH_RANDAO,
+    GET_EPOCH_SYNC_COMMITTEES,
     GET_FINALITY_CHECKPOINT,
     GET_FORK_DATA,
     GET_FORK_SCHEDULE,
     GET_GENESIS,
     GET_HASH_ROOT,
     GET_HEALTH,
+    GET_LIGHT_CLIENT_BOOTSTRAP_STRUCTURE,
+    GET_LIGHT_CLIENT_FINALITY_UPDATE,
+    GET_LIGHT_CLIENT_OPTIMISTIC_UPDATE,
+    GET_LIGHT_CLIENT_UPDATES,
     GET_NODE_IDENTITY,
     GET_PEER,
     GET_PEERS,
     GET_PROPOSER_SLASHINGS,
+    GET_REWARDS,
     GET_SPEC,
     GET_SYNCING,
     GET_VALIDATOR,
@@ -49,15 +59,19 @@ class AsyncBeacon:
     def __init__(
         self,
         base_url: str,
+        request_timeout: float = 10.0,
     ) -> None:
         self.base_url = base_url
+        self.request_timeout = request_timeout
 
     async def _async_make_get_request(self, endpoint_uri: str) -> Dict[str, Any]:
         uri = URI(self.base_url + endpoint_uri)
-        return await async_json_make_get_request(uri)
+        return await async_json_make_get_request(uri, timeout=self.request_timeout)
 
     # [ BEACON endpoints ]
 
+    # states
+
     async def get_genesis(self) -> Dict[str, Any]:
         return await self._async_make_get_request(GET_GENESIS)
 
@@ -90,12 +104,24 @@ class AsyncBeacon:
     async def get_epoch_committees(self, state_id: str = "head") -> Dict[str, Any]:
         return await self._async_make_get_request(GET_EPOCH_COMMITTEES.format(state_id))
 
+    async def get_epoch_sync_committees(self, state_id: str = "head") -> Dict[str, Any]:
+        return await self._async_make_get_request(
+            GET_EPOCH_SYNC_COMMITTEES.format(state_id)
+        )
+
+    async def get_epoch_randao(self, state_id: str = "head") -> Dict[str, Any]:
+        return await self._async_make_get_request(GET_EPOCH_RANDAO.format(state_id))
+
+    # headers
+
     async def get_block_headers(self) -> Dict[str, Any]:
         return await self._async_make_get_request(GET_BLOCK_HEADERS)
 
     async def get_block_header(self, block_id: str) -> Dict[str, Any]:
         return await self._async_make_get_request(GET_BLOCK_HEADER.format(block_id))
 
+    # block
+
     async def get_block(self, block_id: str) -> Dict[str, Any]:
         return await self._async_make_get_request(GET_BLOCK.format(block_id))
 
@@ -107,6 +133,34 @@ class AsyncBeacon:
             GET_BLOCK_ATTESTATIONS.format(block_id)
         )
 
+    async def get_blinded_blocks(self, block_id: str) -> Dict[str, Any]:
+        return await self._async_make_get_request(GET_BLINDED_BLOCKS.format(block_id))
+
+    # rewards
+
+    async def get_rewards(self, block_id: str) -> Dict[str, Any]:
+        return await self._async_make_get_request(GET_REWARDS.format(block_id))
+
+    # light client (untested but follows spec)
+
+    async def get_light_client_bootstrap_structure(
+        self, block_root: HexStr
+    ) -> Dict[str, Any]:
+        return await self._async_make_get_request(
+            GET_LIGHT_CLIENT_BOOTSTRAP_STRUCTURE.format(block_root)
+        )
+
+    async def get_light_client_updates(self) -> Dict[str, Any]:
+        return await self._async_make_get_request(GET_LIGHT_CLIENT_UPDATES)
+
+    async def get_light_client_finality_update(self) -> Dict[str, Any]:
+        return await self._async_make_get_request(GET_LIGHT_CLIENT_FINALITY_UPDATE)
+
+    async def get_light_client_optimistic_update(self) -> Dict[str, Any]:
+        return await self._async_make_get_request(GET_LIGHT_CLIENT_OPTIMISTIC_UPDATE)
+
+    # pool
+
     async def get_attestations(self) -> Dict[str, Any]:
         return await self._async_make_get_request(GET_ATTESTATIONS)
 
@@ -119,6 +173,9 @@ class AsyncBeacon:
     async def get_voluntary_exits(self) -> Dict[str, Any]:
         return await self._async_make_get_request(GET_VOLUNTARY_EXITS)
 
+    async def get_bls_to_execution_changes(self) -> Dict[str, Any]:
+        return await self._async_make_get_request(GET_BLS_TO_EXECUTION_CHANGES)
+
     # [ CONFIG endpoints ]
 
     async def get_fork_schedule(self) -> Dict[str, Any]:
diff --git a/web3/beacon/main.py b/web3/beacon/main.py
index 9db36914..d52a2dbf 100644
--- a/web3/beacon/main.py
+++ b/web3/beacon/main.py
@@ -5,6 +5,7 @@ from typing import (
 
 from eth_typing import (
     URI,
+    HexStr,
 )
 
 from web3._utils.request import (
@@ -16,23 +17,32 @@ from web3.beacon.api_endpoints import (
     GET_ATTESTER_SLASHINGS,
     GET_BEACON_HEADS,
     GET_BEACON_STATE,
+    GET_BLINDED_BLOCKS,
     GET_BLOCK,
     GET_BLOCK_ATTESTATIONS,
     GET_BLOCK_HEADER,
     GET_BLOCK_HEADERS,
     GET_BLOCK_ROOT,
+    GET_BLS_TO_EXECUTION_CHANGES,
     GET_DEPOSIT_CONTRACT,
     GET_EPOCH_COMMITTEES,
+    GET_EPOCH_RANDAO,
+    GET_EPOCH_SYNC_COMMITTEES,
     GET_FINALITY_CHECKPOINT,
     GET_FORK_DATA,
     GET_FORK_SCHEDULE,
     GET_GENESIS,
     GET_HASH_ROOT,
     GET_HEALTH,
+    GET_LIGHT_CLIENT_BOOTSTRAP_STRUCTURE,
+    GET_LIGHT_CLIENT_FINALITY_UPDATE,
+    GET_LIGHT_CLIENT_OPTIMISTIC_UPDATE,
+    GET_LIGHT_CLIENT_UPDATES,
     GET_NODE_IDENTITY,
     GET_PEER,
     GET_PEERS,
     GET_PROPOSER_SLASHINGS,
+    GET_REWARDS,
     GET_SPEC,
     GET_SYNCING,
     GET_VALIDATOR,
@@ -47,15 +57,19 @@ class Beacon:
     def __init__(
         self,
         base_url: str,
+        request_timeout: float = 10.0,
     ) -> None:
         self.base_url = base_url
+        self.request_timeout = request_timeout
 
     def _make_get_request(self, endpoint_url: str) -> Dict[str, Any]:
         uri = URI(self.base_url + endpoint_url)
-        return json_make_get_request(uri)
+        return json_make_get_request(uri, timeout=self.request_timeout)
 
     # [ BEACON endpoints ]
 
+    # states
+
     def get_genesis(self) -> Dict[str, Any]:
         return self._make_get_request(GET_GENESIS)
 
@@ -82,12 +96,22 @@ class Beacon:
     def get_epoch_committees(self, state_id: str = "head") -> Dict[str, Any]:
         return self._make_get_request(GET_EPOCH_COMMITTEES.format(state_id))
 
+    def get_epoch_sync_committees(self, state_id: str = "head") -> Dict[str, Any]:
+        return self._make_get_request(GET_EPOCH_SYNC_COMMITTEES.format(state_id))
+
+    def get_epoch_randao(self, state_id: str = "head") -> Dict[str, Any]:
+        return self._make_get_request(GET_EPOCH_RANDAO.format(state_id))
+
+    # headers
+
     def get_block_headers(self) -> Dict[str, Any]:
         return self._make_get_request(GET_BLOCK_HEADERS)
 
     def get_block_header(self, block_id: str) -> Dict[str, Any]:
         return self._make_get_request(GET_BLOCK_HEADER.format(block_id))
 
+    # blocks
+
     def get_block(self, block_id: str) -> Dict[str, Any]:
         return self._make_get_request(GET_BLOCK.format(block_id))
 
@@ -97,6 +121,34 @@ class Beacon:
     def get_block_attestations(self, block_id: str) -> Dict[str, Any]:
         return self._make_get_request(GET_BLOCK_ATTESTATIONS.format(block_id))
 
+    def get_blinded_blocks(self, block_id: str) -> Dict[str, Any]:
+        return self._make_get_request(GET_BLINDED_BLOCKS.format(block_id))
+
+    # rewards
+
+    def get_rewards(self, block_id: str) -> Dict[str, Any]:
+        return self._make_get_request(GET_REWARDS.format(block_id))
+
+    # light client (untested but follows spec)
+
+    def get_light_client_bootstrap_structure(
+        self, block_root: HexStr
+    ) -> Dict[str, Any]:
+        return self._make_get_request(
+            GET_LIGHT_CLIENT_BOOTSTRAP_STRUCTURE.format(block_root)
+        )
+
+    def get_light_client_updates(self) -> Dict[str, Any]:
+        return self._make_get_request(GET_LIGHT_CLIENT_UPDATES)
+
+    def get_light_client_finality_update(self) -> Dict[str, Any]:
+        return self._make_get_request(GET_LIGHT_CLIENT_FINALITY_UPDATE)
+
+    def get_light_client_optimistic_update(self) -> Dict[str, Any]:
+        return self._make_get_request(GET_LIGHT_CLIENT_OPTIMISTIC_UPDATE)
+
+    # pool
+
     def get_attestations(self) -> Dict[str, Any]:
         return self._make_get_request(GET_ATTESTATIONS)
 
@@ -109,6 +161,9 @@ class Beacon:
     def get_voluntary_exits(self) -> Dict[str, Any]:
         return self._make_get_request(GET_VOLUNTARY_EXITS)
 
+    def get_bls_to_execution_changes(self) -> Dict[str, Any]:
+        return self._make_get_request(GET_BLS_TO_EXECUTION_CHANGES)
+
     # [ CONFIG endpoints ]
 
     def get_fork_schedule(self) -> Dict[str, Any]:
diff --git a/web3/datastructures.py b/web3/datastructures.py
index 13b232d3..0df838e0 100644
--- a/web3/datastructures.py
+++ b/web3/datastructures.py
@@ -14,6 +14,7 @@ from typing import (
     MutableMapping,
     Optional,
     Sequence,
+    Tuple,
     Type,
     TypeVar,
     Union,
@@ -117,7 +118,9 @@ class AttributeDict(ReadableAttributeDict[TKey, TValue], Hashable):
         return hash(tuple(sorted(tupleize_lists_nested(self).items())))
 
     def __eq__(self, other: Any) -> bool:
-        if isinstance(other, Mapping):
+        if isinstance(other, AttributeDict):
+            return hash(self) == hash(other)
+        elif isinstance(other, Mapping):
             return self.__dict__ == dict(other)
         else:
             return False
@@ -130,12 +133,12 @@ def tupleize_lists_nested(d: Mapping[TKey, TValue]) -> AttributeDict[TKey, TValu
     Other unhashable types found will raise a TypeError
     """
 
-    def _to_tuple(lst: List[Any]) -> Any:
-        return tuple(_to_tuple(i) if isinstance(i, list) else i for i in lst)
+    def _to_tuple(value: Union[List[Any], Tuple[Any, ...]]) -> Any:
+        return tuple(_to_tuple(i) if isinstance(i, (list, tuple)) else i for i in value)
 
     ret = dict()
     for k, v in d.items():
-        if isinstance(v, List):
+        if isinstance(v, (list, tuple)):
             ret[k] = _to_tuple(v)
         elif isinstance(v, Mapping):
             ret[k] = tupleize_lists_nested(v)


Test patch:
diff --git a/tests/beacon/test_async_beacon.py b/tests/beacon/test_async_beacon.py
index 532def88..135d8f02 100644
--- a/tests/beacon/test_async_beacon.py
+++ b/tests/beacon/test_async_beacon.py
@@ -3,6 +3,8 @@ from random import (
     randint,
 )
 
+import pytest_asyncio
+
 from web3._utils.request import (
     _async_session_cache,
 )
@@ -10,6 +12,9 @@ from web3.beacon import (
     AsyncBeacon,
 )
 
+# tested against lighthouse which uses port 5052 by default
+BASE_URL = "http://localhost:5052"
+
 
 def _assert_valid_response(response):
     # assert valid response according to Beacon API spec
@@ -19,16 +24,32 @@ def _assert_valid_response(response):
 
 @pytest.fixture
 def async_beacon():
-    # tested against lighthouse which uses port 5052 by default
-    return AsyncBeacon(base_url="http://127.0.0.1:5052")
+    return AsyncBeacon(base_url=BASE_URL)
 
 
-@pytest.fixture(autouse=True)
-def _cleanup():
+@pytest_asyncio.fixture(autouse=True)
+async def _cleanup():
     yield
+    [await session.close() for _, session in _async_session_cache.items()]
     _async_session_cache.clear()
 
 
+# sanity check to make sure the positive test cases are valid
+@pytest.mark.asyncio
+async def test_async_cl_beacon_raises_exception_on_invalid_url(async_beacon):
+    with pytest.raises(ValueError):
+        await async_beacon._async_make_get_request(
+            BASE_URL + "/eth/v1/beacon/nonexistent"
+        )
+
+
+@pytest.mark.asyncio
+async def test_async_beacon_user_request_timeout():
+    beacon = AsyncBeacon(base_url=BASE_URL, request_timeout=0.001)
+    with pytest.raises(TimeoutError):
+        await beacon.get_validators()
+
+
 # Beacon endpoint tests:
 
 
@@ -37,40 +58,30 @@ async def test_async_cl_beacon_get_genesis(async_beacon):
     response = await async_beacon.get_genesis()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_beacon_get_hash_root(async_beacon):
     response = await async_beacon.get_hash_root()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_beacon_get_fork_data(async_beacon):
     response = await async_beacon.get_fork_data()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_beacon_get_finality_checkpoint(async_beacon):
     response = await async_beacon.get_finality_checkpoint()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_beacon_get_validators(async_beacon):
     response = await async_beacon.get_validators()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_beacon_get_validator(async_beacon):
@@ -84,23 +95,29 @@ async def test_async_cl_beacon_get_validator(async_beacon):
     response = await async_beacon.get_validator(random_validator_pubkey)
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_beacon_get_validator_balances(async_beacon):
     response = await async_beacon.get_validator_balances()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_beacon_get_epoch_committees(async_beacon):
     response = await async_beacon.get_epoch_committees()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
+
+@pytest.mark.asyncio
+async def test_async_cl_beacon_get_epoch_sync_committees(async_beacon):
+    response = await async_beacon.get_epoch_sync_committees()
+    _assert_valid_response(response)
+
+
+@pytest.mark.asyncio
+async def test_async_cl_beacon_get_epoch_randao(async_beacon):
+    response = await async_beacon.get_epoch_randao()
+    _assert_valid_response(response)
 
 
 @pytest.mark.asyncio
@@ -108,31 +125,35 @@ async def test_async_cl_beacon_get_block_headers(async_beacon):
     response = await async_beacon.get_block_headers()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_beacon_get_block_header(async_beacon):
     response = await async_beacon.get_block_header("head")
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_beacon_get_block(async_beacon):
     response = await async_beacon.get_block("head")
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_beacon_get_block_root(async_beacon):
     response = await async_beacon.get_block_root("head")
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
+
+@pytest.mark.asyncio
+async def test_async_cl_beacon_get_blinded_blocks(async_beacon):
+    response = await async_beacon.get_blinded_blocks("head")
+    _assert_valid_response(response)
+
+
+@pytest.mark.asyncio
+async def test_async_cl_beacon_get_rewards(async_beacon):
+    response = await async_beacon.get_rewards("head")
+    _assert_valid_response(response)
 
 
 @pytest.mark.asyncio
@@ -140,39 +161,35 @@ async def test_async_cl_beacon_get_block_attestations(async_beacon):
     response = await async_beacon.get_block_attestations("head")
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_beacon_get_attestations(async_beacon):
     response = await async_beacon.get_attestations()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_beacon_get_attester_slashings(async_beacon):
     response = await async_beacon.get_attester_slashings()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_beacon_get_proposer_slashings(async_beacon):
     response = await async_beacon.get_proposer_slashings()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_beacon_get_voluntary_exits(async_beacon):
     response = await async_beacon.get_voluntary_exits()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
+
+@pytest.mark.asyncio
+async def test_async_cl_beacon_get_bls_to_execution_changes(async_beacon):
+    response = await async_beacon.get_bls_to_execution_changes()
+    _assert_valid_response(response)
 
 
 # Config endpoint tests:
@@ -183,24 +200,18 @@ async def test_async_cl_config_get_fork_schedule(async_beacon):
     response = await async_beacon.get_fork_schedule()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_config_get_spec(async_beacon):
     response = await async_beacon.get_spec()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_config_get_deposit_contract(async_beacon):
     response = await async_beacon.get_deposit_contract()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 # Debug endpoint tests:
 
@@ -210,16 +221,12 @@ async def test_async_cl_debug_get_beacon_state(async_beacon):
     response = await async_beacon.get_beacon_state()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_debug_get_beacon_heads(async_beacon):
     response = await async_beacon.get_beacon_heads()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 # Node endpoint tests:
 
@@ -229,31 +236,23 @@ async def test_async_cl_node_get_node_identity(async_beacon):
     response = await async_beacon.get_node_identity()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_node_get_peers(async_beacon):
     response = await async_beacon.get_peers()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_node_get_peer(async_beacon):
     response = await async_beacon.get_peer("")
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_node_get_health(async_beacon):
     response = await async_beacon.get_health()
-    assert response <= 206
-
-    [await session.close() for session in _async_session_cache._data.values()]
+    assert isinstance(response, int)
 
 
 @pytest.mark.asyncio
@@ -261,12 +260,8 @@ async def test_async_cl_node_get_version(async_beacon):
     response = await async_beacon.get_version()
     _assert_valid_response(response)
 
-    [await session.close() for session in _async_session_cache._data.values()]
-
 
 @pytest.mark.asyncio
 async def test_async_cl_node_get_syncing(async_beacon):
     response = await async_beacon.get_syncing()
     _assert_valid_response(response)
-
-    [await session.close() for session in _async_session_cache._data.values()]
diff --git a/tests/beacon/test_beacon.py b/tests/beacon/test_beacon.py
index 03957dd7..bf52a54d 100644
--- a/tests/beacon/test_beacon.py
+++ b/tests/beacon/test_beacon.py
@@ -3,6 +3,10 @@ from random import (
     randint,
 )
 
+from requests import (
+    Timeout,
+)
+
 from web3._utils.request import (
     _session_cache,
 )
@@ -10,6 +14,9 @@ from web3.beacon import (
     Beacon,
 )
 
+# tested against lighthouse which uses port 5052 by default
+BASE_URL = "http://localhost:5052"
+
 
 def _assert_valid_response(response):
     # assert valid response according to Beacon API spec
@@ -19,8 +26,7 @@ def _assert_valid_response(response):
 
 @pytest.fixture
 def beacon():
-    # tested against lighthouse which uses port 5052 by default
-    return Beacon(base_url="http://localhost:5052")
+    return Beacon(base_url=BASE_URL)
 
 
 @pytest.fixture(autouse=True)
@@ -29,6 +35,18 @@ def _cleanup():
     _session_cache.clear()
 
 
+# sanity check to make sure the positive test cases are valid
+def test_cl_beacon_raises_exception_on_invalid_url(beacon):
+    with pytest.raises(ValueError):
+        beacon._make_get_request(BASE_URL + "/eth/v1/beacon/nonexistent")
+
+
+def test_beacon_user_defined_request_timeout():
+    beacon = Beacon(base_url=BASE_URL, request_timeout=0.001)
+    with pytest.raises(Timeout):
+        beacon.get_validators()
+
+
 # Beacon endpoint tests:
 
 
@@ -79,6 +97,16 @@ def test_cl_beacon_get_epoch_committees(beacon):
     _assert_valid_response(response)
 
 
+def test_cl_beacon_get_epoch_sync_committees(beacon):
+    response = beacon.get_epoch_sync_committees()
+    _assert_valid_response(response)
+
+
+def test_cl_beacon_get_epoch_randao(beacon):
+    response = beacon.get_epoch_randao()
+    _assert_valid_response(response)
+
+
 def test_cl_beacon_get_block_headers(beacon):
     response = beacon.get_block_headers()
     _assert_valid_response(response)
@@ -99,6 +127,16 @@ def test_cl_beacon_get_block_root(beacon):
     _assert_valid_response(response)
 
 
+def test_cl_beacon_get_blinded_blocks(beacon):
+    response = beacon.get_blinded_blocks("head")
+    _assert_valid_response(response)
+
+
+def test_cl_beacon_get_rewards(beacon):
+    response = beacon.get_rewards("head")
+    _assert_valid_response(response)
+
+
 def test_cl_beacon_get_block_attestations(beacon):
     response = beacon.get_block_attestations("head")
     _assert_valid_response(response)
@@ -124,6 +162,11 @@ def test_cl_beacon_get_voluntary_exits(beacon):
     _assert_valid_response(response)
 
 
+def test_cl_beacon_get_bls_to_execution_changes(beacon):
+    response = beacon.get_bls_to_execution_changes()
+    _assert_valid_response(response)
+
+
 # Config endpoint tests:
 
 
@@ -175,7 +218,7 @@ def test_cl_node_get_peer(beacon):
 
 def test_cl_node_get_health(beacon):
     response = beacon.get_health()
-    assert response <= 206
+    assert isinstance(response, int)
 
 
 def test_cl_node_get_version(beacon):
diff --git a/tests/core/datastructures/test_tuplelize_nested_lists.py b/tests/core/datastructures/test_datastructures.py
similarity index 52%
rename from tests/core/datastructures/test_tuplelize_nested_lists.py
rename to tests/core/datastructures/test_datastructures.py
index 9a1ac538..2be0af60 100644
--- a/tests/core/datastructures/test_tuplelize_nested_lists.py
+++ b/tests/core/datastructures/test_datastructures.py
@@ -1,4 +1,5 @@
 import pytest
+import random
 import re
 
 from web3.datastructures import (
@@ -7,6 +8,83 @@ from web3.datastructures import (
 )
 
 
+def generate_random_value(depth=0, max_depth=3, key_type=None):
+    if depth > max_depth:
+        return None
+
+    choice = random.choice(["str", "int", "bool", "list", "tuple", "dict"])
+
+    if choice == "str":
+        return "".join(random.choices("abcdefghijklmnopqrstuvwxyz", k=5))
+    elif choice == "int":
+        return random.randint(0, 100)
+    elif choice == "bool":
+        return random.choice([True, False])
+    elif choice == "list":
+        return [
+            generate_random_value(depth + 1, key_type=key_type)
+            for _ in range(random.randint(1, 5))
+        ]
+    elif choice == "tuple":
+        return tuple(
+            generate_random_value(depth + 1, key_type=key_type)
+            for _ in range(random.randint(1, 5))
+        )
+    elif choice == "dict":
+        return generate_random_dict(depth + 1, key_type=key_type)
+
+
+def generate_random_dict(depth=0, max_depth=3, max_keys=5, key_type=None):
+    if not key_type:
+        key_type = random.choice(["str", "int", "float"])
+
+    if depth > max_depth:
+        return {}
+
+    result = {}
+    for _ in range(random.randint(1, max_keys)):
+        if key_type == "str":
+            key = "".join(random.choices("abcdefghijklmnopqrstuvwxyz", k=5))
+        elif key_type == "int":
+            key = random.randint(0, 100)
+        elif key_type == "float":
+            key = round(random.uniform(0, 100), 2)
+
+        value = generate_random_value(depth, max_depth, key_type)
+        result[key] = value
+
+    return AttributeDict.recursive(result)
+
+
+def test_attribute_dict_raises_when_mutated():
+    test_attr_dict = AttributeDict(
+        {"address": "0x4CB06C43fcdABeA22541fcF1F856A6a296448B6c"}
+    )
+    with pytest.raises(TypeError):
+        # should raise when trying to edit an attribute with dict syntax
+        test_attr_dict["address"] = "0x6C8f2A135f6ed072DE4503Bd7C4999a1a17F824B"
+
+    with pytest.raises(TypeError):
+        # should raise when trying to edit an attribute with dot syntax
+        test_attr_dict.address = "0x6C8f2A135f6ed072DE4503Bd7C4999a1a17F824B"
+
+    with pytest.raises(TypeError):
+        # should raise when trying to add an attribute with dict syntax
+        test_attr_dict["cats"] = "0x6C8f2A135f6ed072DE4503Bd7C4999a1a17F824B"
+
+    with pytest.raises(TypeError):
+        # should raise when trying to add an attribute with dot syntax
+        test_attr_dict.cats = "0x6C8f2A135f6ed072DE4503Bd7C4999a1a17F824B"
+
+    with pytest.raises(TypeError):
+        # should raise when trying to delete an attribute with dict syntax
+        del test_attr_dict["address"]
+
+    with pytest.raises(TypeError):
+        # should raise when trying to delete an attribute with dot syntax
+        del test_attr_dict.address
+
+
 @pytest.mark.parametrize(
     "input,expected",
     (
@@ -50,9 +128,18 @@ from web3.datastructures import (
         ),
     ),
 )
-def test_tupleization_and_hashing(input, expected):
+def test_tupleization_and_hashing_passing_defined(input, expected):
     assert tupleize_lists_nested(input) == expected
     assert hash(AttributeDict(input)) == hash(expected)
+    assert AttributeDict(input) == expected
+
+
+def test_tupleization_and_hashing_passing_random():
+    for _ in range(1000):
+        random_dict = generate_random_dict()
+        tupleized_random_dict = tupleize_lists_nested(random_dict)
+        assert hash(random_dict) == hash(tupleized_random_dict)
+        assert random_dict == tupleized_random_dict
 
 
 @pytest.mark.parametrize(